package studio.baka.satoripixeldungeon.levels.rooms;

import studio.baka.satoripixeldungeon.levels.Level;
import com.watabou.utils.*;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;

public abstract class Room extends Rect implements Graph.Node, Bundlable {

    public ArrayList<Room> neigbours = new ArrayList<>();
    public LinkedHashMap<Room, Door> connected = new LinkedHashMap<>();

    public int distance;
    public int price = 1;

    public Room() {
        super();
    }

    public Room(Rect other) {
        super(other);
    }

    public Room set(Room other) {
        super.set(other);
        for (Room r : other.neigbours) {
            neigbours.add(r);
            r.neigbours.remove(other);
            r.neigbours.add(this);
        }
        for (Room r : other.connected.keySet()) {
            Door d = other.connected.get(r);
            r.connected.remove(other);
            r.connected.put(this, d);
            connected.put(r, d);
        }
        return this;
    }

    // **** Spatial logic ****

    //Note: when overriding these YOU MUST store any randomly decided values.
    //With the same room and the same parameters these should always return
    //the same value over multiple calls, even if there's some randomness initially.
    public int minWidth() {
        return -1;
    }

    public int maxWidth() {
        return -1;
    }

    public int minHeight() {
        return -1;
    }

    public int maxHeight() {
        return -1;
    }

    public boolean setSize() {
        return setSize(minWidth(), maxWidth(), minHeight(), maxHeight());
    }

    public boolean forceSize(int w, int h) {
        return setSize(w, w, h, h);
    }

    public boolean setSizeWithLimit(int w, int h) {
        if (w < minWidth() || h < minHeight()) {
            return false;
        } else {
            setSize();

            if (width() > w || height() > h) {
                resize(Math.min(width(), w) - 1, Math.min(height(), h) - 1);
            }

            return true;
        }
    }

    protected boolean setSize(int minW, int maxW, int minH, int maxH) {
        if (minW < minWidth()
                || maxW > maxWidth()
                || minH < minHeight()
                || maxH > maxHeight()
                || minW > maxW
                || minH > maxH) {
            return false;
        } else {
            //subtract one because rooms are inclusive to their right and bottom sides
            resize(Random.NormalIntRange(minW, maxW) - 1,
                    Random.NormalIntRange(minH, maxH) - 1);
            return true;
        }
    }

    //Width and height are increased by 1 because rooms are inclusive to their right and bottom sides
    @Override
    public int width() {
        return super.width() + 1;
    }

    @Override
    public int height() {
        return super.height() + 1;
    }

    public Point random() {
        return random(1);
    }

    public Point random(int m) {
        return new Point(Random.IntRange(left + m, right - m),
                Random.IntRange(top + m, bottom - m));
    }

    //a point is only considered to be inside if it is within the 1 tile perimeter
    public boolean inside(Point p) {
        return p.x > left && p.y > top && p.x < right && p.y < bottom;
    }

    public Point center() {
        return new Point(
                (left + right) / 2 + (((right - left) % 2) == 1 ? Random.Int(2) : 0),
                (top + bottom) / 2 + (((bottom - top) % 2) == 1 ? Random.Int(2) : 0));
    }


    // **** Connection logic ****

    public static final int ALL = 0;
    public static final int LEFT = 1;
    public static final int TOP = 2;
    public static final int RIGHT = 3;
    public static final int BOTTOM = 4;

    public int minConnections(int direction) {
        if (direction == ALL) return 1;
        else return 0;
    }

    public int curConnections(int direction) {
        if (direction == ALL) {
            return connected.size();

        } else {
            int total = 0;
            for (Room r : connected.keySet()) {
                Rect i = intersect(r);
                if (direction == LEFT && i.width() == 0 && i.left == left) total++;
                else if (direction == TOP && i.height() == 0 && i.top == top) total++;
                else if (direction == RIGHT && i.width() == 0 && i.right == right) total++;
                else if (direction == BOTTOM && i.height() == 0 && i.bottom == bottom) total++;
            }
            return total;
        }
    }

    public int remConnections(int direction) {
        if (curConnections(ALL) >= maxConnections(ALL)) return 0;
        else return maxConnections(direction) - curConnections(direction);
    }

    public int maxConnections(int direction) {
        if (direction == ALL) return 16;
        else return 4;
    }

    //only considers point-specific limits, not direction limits
    public boolean canConnect(Point p) {
        //point must be along exactly one edge, no corners.
        return (p.x == left || p.x == right) != (p.y == top || p.y == bottom);
    }

    //only considers direction limits, not point-specific limits
    public boolean canConnect(int direction) {
        return remConnections(direction) > 0;
    }

    //considers both direction and point limits
    public boolean canConnect(Room r) {
        Rect i = intersect(r);

        boolean foundPoint = false;
        for (Point p : i.getPoints()) {
            if (canConnect(p) && r.canConnect(p)) {
                foundPoint = true;
                break;
            }
        }
        if (!foundPoint) return false;

        if (i.width() == 0 && i.left == left)
            return canConnect(LEFT) && r.canConnect(LEFT);
        else if (i.height() == 0 && i.top == top)
            return canConnect(TOP) && r.canConnect(TOP);
        else if (i.width() == 0 && i.right == right)
            return canConnect(RIGHT) && r.canConnect(RIGHT);
        else if (i.height() == 0 && i.bottom == bottom)
            return canConnect(BOTTOM) && r.canConnect(BOTTOM);
        else
            return false;
    }

    public boolean addNeigbour(Room other) {
        if (neigbours.contains(other))
            return true;

        Rect i = intersect(other);
        if ((i.width() == 0 && i.height() >= 2) ||
                (i.height() == 0 && i.width() >= 2)) {
            neigbours.add(other);
            other.neigbours.add(this);
            return true;
        }
        return false;
    }

    public boolean connect(Room room) {
        if ((neigbours.contains(room) || addNeigbour(room))
                && !connected.containsKey(room) && canConnect(room)) {
            connected.put(room, null);
            room.connected.put(this, null);
            return true;
        }
        return false;
    }

    public void clearConnections() {
        for (Room r : neigbours) {
            r.neigbours.remove(this);
        }
        neigbours.clear();
        for (Room r : connected.keySet()) {
            r.connected.remove(this);
        }
        connected.clear();
    }

    // **** Painter Logic ****

    public abstract void paint(Level level);

    //whether or not a painter can make its own modifications to a specific point
    public boolean canPlaceWater(Point p) {
        return inside(p);
    }

    public final ArrayList<Point> waterPlaceablePoints() {
        ArrayList<Point> points = new ArrayList<>();
        for (int i = left; i <= right; i++) {
            for (int j = top; j <= bottom; j++) {
                Point p = new Point(i, j);
                if (canPlaceWater(p)) points.add(p);
            }
        }
        return points;
    }

    //whether or not a painter can make place grass at a specific point
    public boolean canPlaceGrass(Point p) {
        return inside(p);
    }

    public final ArrayList<Point> grassPlaceablePoints() {
        ArrayList<Point> points = new ArrayList<>();
        for (int i = left; i <= right; i++) {
            for (int j = top; j <= bottom; j++) {
                Point p = new Point(i, j);
                if (canPlaceGrass(p)) points.add(p);
            }
        }
        return points;
    }

    //whether or not a painter can place a trap at a specific point
    public boolean canPlaceTrap(Point p) {
        return inside(p);
    }

    public final ArrayList<Point> trapPlaceablePoints() {
        ArrayList<Point> points = new ArrayList<>();
        for (int i = left; i <= right; i++) {
            for (int j = top; j <= bottom; j++) {
                Point p = new Point(i, j);
                if (canPlaceTrap(p)) points.add(p);
            }
        }
        return points;
    }

    //whether or not a character (usually spawned) can be placed here
    public boolean canPlaceCharacter(Point p, Level l) {
        return inside(p);
    }

    public final ArrayList<Point> charPlaceablePoints(Level l) {
        ArrayList<Point> points = new ArrayList<>();
        for (int i = left; i <= right; i++) {
            for (int j = top; j <= bottom; j++) {
                Point p = new Point(i, j);
                if (canPlaceCharacter(p, l)) points.add(p);
            }
        }
        return points;
    }

    // **** Graph.Node interface ****

    @Override
    public int distance() {
        return distance;
    }

    @Override
    public void distance(int value) {
        distance = value;
    }

    @Override
    public int price() {
        return price;
    }

    @Override
    public void price(int value) {
        price = value;
    }

    @Override
    public Collection<Room> edges() {
        ArrayList<Room> edges = new ArrayList<>();
        for (Room r : connected.keySet()) {
            Door d = connected.get(r);
            //for the purposes of path building, ignore all doors that are locked, blocked, or hidden
            if (d.type == Door.Type.EMPTY || d.type == Door.Type.TUNNEL
                    || d.type == Door.Type.UNLOCKED || d.type == Door.Type.REGULAR) {
                edges.add(r);
            }
        }
        return edges;
    }

    @Override
    public void storeInBundle(Bundle bundle) {
        bundle.put("left", left);
        bundle.put("top", top);
        bundle.put("right", right);
        bundle.put("bottom", bottom);
    }

    @Override
    public void restoreFromBundle(Bundle bundle) {
        left = bundle.getInt("left");
        top = bundle.getInt("top");
        right = bundle.getInt("right");
        bottom = bundle.getInt("bottom");
    }

    //FIXME currently connections and neighbours are not preserved on load
    public void onLevelLoad(Level level) {
        //does nothing by default
    }

    public static class Door extends Point implements Bundlable {

        public enum Type {
            EMPTY, TUNNEL, REGULAR, UNLOCKED, HIDDEN, BARRICADE, LOCKED
        }

        public Type type = Type.EMPTY;

        public Door() {
        }

        public Door(Point p) {
            super(p);
        }

        public Door(int x, int y) {
            super(x, y);
        }

        public void set(Type type) {
            if (type.compareTo(this.type) > 0) {
                this.type = type;
            }
        }

        @Override
        public void storeInBundle(Bundle bundle) {
            bundle.put("x", x);
            bundle.put("y", y);
            bundle.put("type", type);
        }

        @Override
        public void restoreFromBundle(Bundle bundle) {
            x = bundle.getInt("x");
            y = bundle.getInt("y");
            type = bundle.getEnum("type", Type.class);
        }
    }
}